package com.sinszm.wx.pay;

import com.googlecode.protobuf.format.JsonJacksonFormat;
import com.sinszm.common.exception.ApiException;
import com.sinszm.common.util.SignUtils;
import com.sinszm.common.util.XmlUtils;
import com.sinszm.wx.properties.BasicWxProperties;
import com.sinszm.wx.protocol.Wx;
import org.nutz.json.Json;
import org.nutz.json.JsonFormat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;

import static com.sinszm.wx.exception.WxApiError.*;

/**
 * 支付通用抽象
 *
 * @author chenjianbo
 */
@Component
public abstract class AbstractPay {

    private final BasicWxProperties properties;
    private final HttpServletRequest request;
    private final JsonJacksonFormat jsonJacksonFormat;

    @Autowired
    protected AbstractPay(BasicWxProperties properties, HttpServletRequest request, JsonJacksonFormat jsonJacksonFormat) {
        this.properties = properties;
        this.request = request;
        this.jsonJacksonFormat = jsonJacksonFormat;
    }

    public BasicWxProperties getProperties() {
        return properties;
    }

    public HttpServletRequest getRequest() {
        return request;
    }

    private JsonJacksonFormat getJsonJacksonFormat() {
        return jsonJacksonFormat;
    }

    /**
     * 格式化成有序map
     * @param params
     */
    @SuppressWarnings("unchecked")
    private SortedMap<String, String> format(String params) {
        if (StringUtils.isEmpty(params)) {
            throw new ApiException(WX_PAY_ERROR_002);
        }
        return Json.fromJson(TreeMap.class, params);
    }

    /**
     * 获取签名类型
     */
    private String signType() {
        SignUtils.Method method = properties.getSignType();
        switch (method) {
            case MD5:
                return "MD5";
            case HMAC_SHA256:
                return "HMAC-SHA256";
            default:
                return "SHA1";
        }
    }

    /**
     * 获取请求地址
     * @param u
     */
    public String getUrl(String u) {
        return properties.getPayApi() + u;
    }

    /**
     * 获取请求参数信息
     * @param request
     */
    public String notifyParam(HttpServletRequest request) {
        try(ByteArrayOutputStream bao = new ByteArrayOutputStream();
            InputStream in = request.getInputStream()) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) != -1) {
                bao.write(buffer, 0, len);
            }
            return new String(bao.toByteArray(),"UTF-8");
        } catch (IOException e) {
            e.printStackTrace(System.out);
            throw new ApiException(WX_PAY_ERROR_016);
        }
    }

    /**
     * 获取请求XML内容
     * @param payReq        请求常规参数
     * @param scene         场景信息1
     * @param scenePro      场景信息2
     * @param trades        上报内容
     * @return              数组：第一位表示证书密钥,第二位表示xml内容,第三位表示商户ID
     */
    public String[] fetchReqXml(
            Wx.Bean.PayReq payReq,
            Wx.Bean.SceneInfo scene,
            Wx.Bean.SceneInfoPro scenePro,
            List<Wx.Bean.Trades> trades) {
        return fetchReqXml(payReq, scene, scenePro, trades, null);
    }

    /**
     * 获取请求XML内容
     * @param payReq        请求常规参数
     * @param scene         场景信息1
     * @param scenePro      场景信息2
     * @param trades        上报内容
     * @param changeKey     字段值进行改变：找到key更改为changeKey对应的value
     * @return              数组：第一位表示证书密钥,第二位表示xml内容,第三位表示商户ID
     */
    public String[] fetchReqXml(
            Wx.Bean.PayReq payReq,
            Wx.Bean.SceneInfo scene,
            Wx.Bean.SceneInfoPro scenePro,
            List<Wx.Bean.Trades> trades, Map<String, String> changeKey) {
        if (payReq == null) {
            throw new ApiException(WX_PAY_ERROR_002);
        }
        //获取证书密钥
        String certKey = payReq.getCertKey();
        //处理个别参数
        Wx.Bean.PayReq.Builder reqBuilder = payReq.toBuilder()
                .setSign("")
                .setCertKey("")
                .setSignType(signType());
        //处理场景信息1
        if (scene != null) {
            reqBuilder.setSceneInfo(
                    getJsonJacksonFormat().printToString(scene)
            );
        }
        //处理场景信息2
        if (scenePro != null) {
            reqBuilder.setSceneInfo(
                    getJsonJacksonFormat().printToString(scenePro)
            );
        }
        //处理上报参数
        if (trades != null && !trades.isEmpty()) {
            List<SortedMap<String, String>> tradesList = trades.stream().map(mapper -> format(
                    getJsonJacksonFormat().printToString(mapper)
            )).collect(Collectors.toList());
            reqBuilder.setTrades(Json.toJson(tradesList, JsonFormat.tidy()));
        }
        //字符串格式化
        Wx.Bean.PayReq req = reqBuilder.build();
        String reqStr = getJsonJacksonFormat().printToString(req);
        //处理签名
        SortedMap<String,String> params = format(reqStr);
        //执行key转换
        if (changeKey != null && !changeKey.isEmpty()) {
            changeKey.forEach((k, v) -> {
                String value = params.get(k);
                params.put(v, value);
                params.remove(k);
            });
        }
        String sign = SignUtils.createSign(params, certKey, getProperties().getSignType());
        //签名回写并格式化
        params.put("sign", sign);
        //XML转换响应
        return new String[]{
                certKey,
                XmlUtils.basicMapToXmlNice(params),
                req.getMchId()
        };
    }

    /**
     * 获取返回值内容
     *
     * @param response
     */
    public Wx.Bean.PayResp fetchRespBody(SortedMap<String, String> response) {
        String content = Json.toJson(response, JsonFormat.tidy());
        Wx.Bean.PayResp.Builder builder = Wx.Bean.PayResp.newBuilder();
        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content.getBytes())) {
            getJsonJacksonFormat().merge(inputStream, builder);
            //处理特殊字段的数据合并
            merge(response, builder);
            //返回响应结果
            return builder.build();
        } catch (IOException e) {
            e.printStackTrace(System.err);
            throw new ApiException(WX_PAY_ERROR_013);
        }
    }

    private void merge(SortedMap<String, String> response, Wx.Bean.PayResp.Builder builder) {
        final String ch = "_$";
        List<SpecialAttr> specials = new ArrayList<>();
        response.forEach((k, v) -> {
            if (k.contains(ch)) {
                specials.add(new SpecialAttr(k.replaceAll("[$][(0-9)|(1-9)(0-9)]", "z"), k, v));
            }
        });
        if (!specials.isEmpty()) {
            Map<String, List<SpecialAttr>> group = specials.stream()
                    .collect(Collectors.groupingBy(SpecialAttr::getId));
            String json = Json.toJson(group, JsonFormat.tidy());
            try( ByteArrayInputStream ism = new ByteArrayInputStream(json.getBytes()) ) {
                getJsonJacksonFormat().merge(ism, builder);
            } catch (IOException e) {
                e.printStackTrace(System.err);
                throw new ApiException(WX_PAY_ERROR_017);
            }
        }
    }

    private static class SpecialAttr{
        private String id;
        private String name;
        private String content;

        SpecialAttr(String id, String name, String content) {
            this.id = id;
            this.name = name;
            this.content = content;
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getContent() {
            return content;
        }

        public void setContent(String content) {
            this.content = content;
        }
    }

}